package com.creatified.jacqueline;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathException;
import javax.xml.xpath.XPathFactory;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.w3c.tidy.Tidy;


/**
 * Your main entry point when using the features of Jacqueline.
 * Jacqueline lets you add a list of CSS files and a list of HTML documents at creation time.
 * It will check the CSS selectors retrieved from the CSS files against the HTML documents and return a JacquelineResult for this.
 */
public class Jacqueline {

	protected final static Logger LOG = Logger.getLogger(Jacqueline.class);

	private final List<File> cssFiles;
	
	private final List<Document> htmlDocuments;
	
	private final List<JacquelineSelector> cssSelectors;

	/**
	 * Instantiates a new jacqueline. From a List of Files as CSS files and a List of Documents as HTML documents.
	 * 
	 * @param cssFiles the css files to extract CSS rules/selectors from
	 * @param htmlDocuments the html documents to check against
	 */
	public Jacqueline(List<File> cssFiles, List<Document> htmlDocuments) throws IOException {
		this.cssFiles = cssFiles;
		this.htmlDocuments = htmlDocuments;
		cssSelectors = new ArrayList<JacquelineSelector>();

		cssSelectors.addAll(getSelectors(this.cssFiles));
	}

	/**
	 * Instantiates a new jacqueline. From only one File as CSS file and one Document as HTML document.
	 * 
	 * @param cssFile the CSS file
	 * @param doc the document
	 */
	public Jacqueline(File cssFile, Document doc) throws IOException {
		cssFiles = new ArrayList<File>();
		htmlDocuments = new ArrayList<Document>();

		cssFiles.add(cssFile);
		htmlDocuments.add(doc);

		cssSelectors = new ArrayList<JacquelineSelector>();
		cssSelectors.addAll(getSelectors(this.cssFiles));
	}

	/**
	 * Instantiates a new jacqueline from a single CSS file and several documents.
	 * 
	 * @param cssFile the CSS file
	 * @param docs the documents
	 */
	public Jacqueline(File cssFile, List<Document> docs) throws IOException {
		cssFiles = new ArrayList<File>();
		cssFiles.add(cssFile);

		htmlDocuments = docs;

		cssSelectors = new ArrayList<JacquelineSelector>();
		cssSelectors.addAll(getSelectors(this.cssFiles));
	}

	/**
	 * Processes all the documents and CSS files associated with this Jacqueline instance
	 * 
	 * @return the resulting JacquelineResult
	 */
	public JacquelineResult process() throws IOException, XPathException {
		List<JacquelineResult> results = new ArrayList<JacquelineResult>();

		LOG.info("Checking " + htmlDocuments.size() + " document(s)");

		int i = 0;
		long start = 0;
		for(Document doc: htmlDocuments) {
			LOG.info("Checking document " + (i+1) + "/" + htmlDocuments.size());

			start = System.currentTimeMillis();

			results.add(testDocument(doc));

			LOG.info("Finished after: " + (System.currentTimeMillis() - start) + "ms");

			i++;
		}

		JacquelineResult result = new JacquelineResult();
		for(JacquelineResult r: results) {
			LOG.info("merging results: " + result.getSelectors().size() + ":" + r.getSelectors().size());
			start = System.currentTimeMillis();
			result = JacquelineResult.mergeResults(result, r);
			LOG.info("Merged result contains " + result.size() + " entries");
			LOG.info("Finished after: " + (System.currentTimeMillis() - start) + "ms");
		}
		return result;
	}

	/**
	 * Tests a single document.
	 * 
	 * @param doc the document
	 * 
	 * @return the JacquelineResult for this document
	 */
	private JacquelineResult testDocument(Document doc) throws IOException, XPathException {

		JacquelineResult jackieResult = new JacquelineResult();

		XPathFactory factory = XPathFactory.newInstance();
		XPath xpath = factory.newXPath();

		NodeList nodes = null;

		for(JacquelineSelector selector : cssSelectors) {
			nodes = (NodeList) xpath.compile(selector.getXPathExpression()).evaluate(doc, XPathConstants.NODESET);

			if(nodes != null && nodes.getLength() > 0) {
				/*
				 * WORKAROUND
				 * 'timesUsed' can very big for selectors like '*' (even to big for type long)
				 * so we'll only increment 'timesUsed' by 1 for each document having a match
				 */
				selector.setTimesUsed(1);
			}

			LOG.debug(selector + " was found " + nodes.getLength() + " time(s) in document");

			jackieResult.add(selector);

		}

		return jackieResult;
	}

	/**
	 * Utility method to create a Document from a FileInputStream
	 * 
	 * @param f the FileInputStream to read from
	 * 
	 * @return the generated Document
	 */
	public static Document generateDocument(FileInputStream f) {
		Tidy tidy = new Tidy();

		tidy.setXHTML(false);
		tidy.setQuiet(true);
		tidy.setShowWarnings(false);

		return tidy.parseDOM(f, null);
	}

	/**
	 * Generates a document from a file
	 * 
	 * @param file the file to read from
	 * 
	 * @return the generated document
	 * 
	 * @throws FileNotFoundException if the File could not be found
	 */
	public static Document generateDocument(File file) throws FileNotFoundException {
		return generateDocument(new FileInputStream(file));
	}

	/**
	 * Generates a document from a file referenced by an abstract path.
	 * 
	 * @param filename the path to the file
	 * 
	 * @return the generated document
	 * 
	 * @throws FileNotFoundException if the file could not be found
	 */
	public static Document generateDocument(String filename) throws FileNotFoundException {
		return generateDocument(new FileInputStream(filename));
	}
	
	/**
	 * Gets the CSS selectors from several files
	 * 
	 * @return the selectors
	 */
	private List<JacquelineSelector> getSelectors(List<File> cssFiles) throws IOException {
		List<JacquelineSelector> selectors = new ArrayList<JacquelineSelector>();

		for(File f: cssFiles) {
			selectors.addAll(getSelectorsFromCss(f));
		}

		return selectors;
	}

	/**
	 * Utility method to retrieve all selectors from a CSS file
	 * 
	 * @param f the file to read from
	 * 
	 * @return the parsed CSS selectors as a List of JacquelineSelector instances
	 */
	private static ArrayList<JacquelineSelector> getSelectorsFromCss(File f) throws IOException {
		JacquelineCSSParser jf = new JacquelineCSSParser(f);

		LOG.debug("found " + jf.getSelectors().size() + " selectors in document '" + f.getPath() + "'");

		return jf.getSelectors();
	}


}
